﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.IO;

namespace aerowan.common.Network
{
    /// <summary>
    /// This class represents a connection to a remote end
    /// </summary>
    public class NetConnection
    {
        /// <summary>
        /// Local socket instance to the remote end
        /// </summary>
        public Socket Client { get; private set; }
        /// <summary>
        /// Receive buffer for socket (4096 bytes)
        /// </summary>
        public byte[] ReceiveBuffer { get; private set; }
        /// <summary>
        /// Used to store partial packets prior to processing
        /// </summary>
        MemoryStream ReceiveStream { get; set; }

        /// <summary>
        /// If Client connection, address of the server
        /// </summary>
        public IPAddress ServerAddress { get; private set; }
        /// <summary>
        /// If Client connection, port on the remote server
        /// </summary>
        public int ServerPort { get; private set; }

        /// <summary>
        /// Gets or sets an object to use as attachment with this connection
        /// </summary>
        public object Attachment { get; set; }

        /// <summary>
        /// Address of the remote end
        /// </summary>
        public string RemoteIP
        {
            get
            {
                return (Client.RemoteEndPoint as IPEndPoint).Address.ToString();
            }
        }

        /// <summary>
        /// Determines whether this connection is client to server.
        /// </summary>
        bool IsClient = false;
        /// <summary>
        /// Determines whether the initial RSA handshake has been completed.
        /// </summary>
        bool RSAHandshakeComplete = false;

        public delegate void NetConnectionClosedEventHandler();
        public event NetConnectionClosedEventHandler Closed;

        /// <summary>
        /// Connect to Server
        /// </summary>
        /// <param name="address">Server Address</param>
        /// <param name="port">Server Port</param>
        public NetConnection(IPAddress address, int port)
        {
            Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            ServerAddress = address;
            ServerPort = port;
            IsClient = true;
        }

        /// <summary>
        /// Client Socket accepted
        /// </summary>
        /// <param name="socket"></param>
        public NetConnection(Socket socket)
        {
            Client = socket;
            IsClient = false;
            Console.WriteLine("Client " + RemoteIP + " connected.");
            Init();
        }

        /// <summary>
        /// Before anything done with the connection
        /// </summary>
        public bool Init()
        {
            ReceiveBuffer = new byte[4096];
            ReceiveStream = new MemoryStream();
            if (IsClient)
            {
                try
                {
                    Client.Connect(new IPEndPoint(ServerAddress, ServerPort));
                }
                catch (Exception e)
                {
                    return false;
                }
            }
            BeginRead();
            return true;
        }

        public void OnDisconnect(bool close)
        {
            try
            {
                Console.WriteLine("Client disconnecting.");
                if(close) Client.Close();
                if (Closed != null) Closed();
            }
            catch (Exception e)
            {
                Console.WriteLine("Error while closing connection: " + e.ToString());
            }
        }

        /// <summary>
        /// Start read from the socket
        /// </summary>
        public void BeginRead()
        {
            Client.BeginReceive(ReceiveBuffer, 0, 4096, SocketFlags.None, new AsyncCallback(EndRead), null);
        }

        void EndRead(IAsyncResult ar)
        {
            try
            {
                int recvBytes = Client.EndReceive(ar);
                if (recvBytes <= 0 || !Client.Connected)
                {
                    OnDisconnect(true);
                }
                
                Console.WriteLine("Recv " + recvBytes + " from client.");

                if (ReceiveStream.Position == 0)
                {
                    // Nothing in stream. Read length of current packet.
                    short packetSize = BitConverter.ToInt16(ReceiveBuffer, 0);
                    Console.WriteLine("Read " + packetSize + " in packet header.");
                    if (packetSize == recvBytes)
                    {
                        // This is a full packet. Directly process.
                        byte[] temp = new byte[recvBytes];
                        Array.Copy(ReceiveBuffer, 0, temp, 0, recvBytes);
                        ProcessIncomingPacket(temp);
                    }
                    else if (packetSize > recvBytes)
                    {
                        // This is only a partial packet. Append to stream.
                        ReceiveStream.Write(ReceiveBuffer, 0, recvBytes);
                    }
                }
                else
                {
                    // There is something in stream. Write current payload and check.
                    ReceiveStream.Write(ReceiveBuffer, 0, recvBytes);
                    long currentPosition = ReceiveStream.Position;
                    ReceiveStream.Position = 0;
                    short streamWrittenSize = ReceiveStream.ReadShort();
                    if (streamWrittenSize == currentPosition)
                    {
                        // We got the full packet. Process it.
                        ReceiveStream.SetLength(streamWrittenSize);
                        ProcessIncomingPacket(ReceiveStream.ToArray());
                        ReceiveStream.Position = 0;
                        ReceiveStream.SetLength(0);
                    }
                    else
                    {
                        // Once again a partial packet ...
                        ReceiveStream.Position = currentPosition;
                    }
                }

                BeginRead();
            }
            catch (Exception e)
            {
                if (!(e is SocketException) && !(e is ObjectDisposedException))
                {
                    Console.WriteLine("Socket::EndRead Exception: " + e.ToString());
                }
                OnDisconnect(false);
            }
        }

        void ProcessIncomingPacket(byte[] data)
        {
            Console.WriteLine("Processing " + data.Length + " bytes as remote packet.");
            int opcode = BitConverter.ToInt32(data, 2);
            Console.WriteLine("Opcode = " + opcode);
            Type clientPacketType = PacketTypeManager.GetClientPacketType(opcode);
            if (clientPacketType == null)
            {
                Console.WriteLine("Received packet with unk opcode " + opcode);
                return;
            }
            BaseRemotePacket pkt = (BaseRemotePacket)Activator.CreateInstance(clientPacketType, data);
            pkt.ReadPacket(this);
            pkt.RunPacket(this);
        }

        bool closeAtNextPacket = false;

        /// <summary>
        /// Instructs the connection to close itself automatically after next packet is done being sent.
        /// </summary>
        public void CloseAtNextPacket() { closeAtNextPacket = true; }

        public void BeginSend(byte[] data)
        {
            Client.BeginSend(data, 0, data.Length, SocketFlags.None, new AsyncCallback(EndSend), null);
        }

        void EndSend(IAsyncResult ar)
        {
            int sent = Client.EndSend(ar);
            if (closeAtNextPacket)
            {
                OnDisconnect(true);
            }
        }

        /// <summary>
        /// Send a structured packet through the socket.
        /// Encryption will be RSA or Salsa depending on connection state.
        /// </summary>
        /// <param name="pkt"></param>
        public void SendPacket(BaseLocalPacket pkt)
        {
            pkt.WritePacket(this); // Write Packet Contents
            short position = (short)pkt.Position;
            pkt.Position = 0;
            pkt.WriteShort(position);
            BeginSend(pkt.ToArray());
        }
    }
}
